home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / python-support / gnome-games-data / gnome_sudoku / gsudoku.py < prev    next >
Encoding:
Python Source  |  2009-04-14  |  52.8 KB  |  1,462 lines

  1. # -*- coding: utf-8 -*-
  2. import gtk, cairo, pango, gobject
  3. import colors
  4. import math
  5. import random
  6. from simple_debug import simple_debug
  7. from gettext import gettext as _
  8. import sudoku
  9.  
  10. TRACKER_COLORS = [
  11.     # Use tango colors recommended here:
  12.     # http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines
  13.     tuple([x/255.0 for x in cols]) for cols in
  14.     [(32,74,135), # Sky Blue 3
  15.      (78,154,6), # Chameleon 3
  16.      (206,92,0), # Orange 3
  17.      (143,89,2), # Chocolate 3
  18.      (92,53,102), # Plum 3
  19.      (85,87,83), # Aluminium 5
  20.      (196,160,0), # Butter 3
  21.  
  22.      ]
  23.     ]
  24.  
  25. def gtkcolor_to_rgb (c):
  26.     return c.red/float(2**16),c.green/float(2**16),c.blue/float(2**16)
  27.  
  28. def overlay (color_1, color_2, method=1):
  29.     return color_1[0]+color_2[0]*method,color_1[1]+color_2[1]*method,color_1[2]+color_2[2]*method
  30.  
  31. ERROR_HIGHLIGHT_COLOR = (1.0,0,0)
  32.  
  33. BASE_SIZE = 35 # The "normal" size of a box (in pixels)
  34.  
  35. # And the standard font-sizes -- these should fit nicely with the
  36. # BASE_SIZE
  37. BASE_FONT_SIZE = pango.SCALE * 13 
  38. NOTE_FONT_SIZE = pango.SCALE * 6
  39.  
  40. BORDER_WIDTH = 9.0 # The size of space we leave for a box
  41.  
  42. BORDER_LINE_WIDTH = 4 # The size of the line we draw around a selected box
  43.  
  44. LITTLE_LINE_WIDTH = 0.25
  45. NORMAL_LINE_WIDTH = 1 # The size of the line we draw around a box
  46.  
  47. SPACING_FACTOR = 40 # The size of a box compared (roughly) to the size
  48.                     # of padding -- the larger this is, the smaller
  49.                     # the spaces
  50. SMALL_TO_BIG_FACTOR = 3.5 # The number of times wider than a small line a big line is.
  51.  
  52. class NumberSelector (gtk.EventBox):
  53.  
  54.     __gsignals__ = {
  55.         'changed':(gobject.SIGNAL_RUN_LAST,gobject.TYPE_NONE,()),
  56.         }
  57.     
  58.     def __init__ (self,default=None,upper=9):
  59.         self.value = default
  60.         gtk.EventBox.__init__(self)
  61.         self.table = gtk.Table()
  62.         self.add(self.table)
  63.         side = int(math.sqrt(upper))
  64.         n = 1
  65.         for y in range(side):
  66.             for x in range(side):
  67.                 b = gtk.Button()
  68.                 l = gtk.Label()
  69.                 if n==self.value:
  70.                     l.set_markup('<b><span size="x-small">%s</span></b>'%n)
  71.                 else:
  72.                     l.set_markup('<span size="x-small">%s</span>'%n)
  73.                 b.add(l)
  74.                 b.set_relief(gtk.RELIEF_HALF)
  75.                 l = b.get_children()[0]
  76.                 b.set_border_width(0)
  77.                 l.set_padding(0,0)
  78.                 l.get_alignment()
  79.                 b.connect('clicked',self.number_clicked,n)
  80.                 self.table.attach(b,x,x+1,y,y+1)
  81.                 n+=1
  82.         if self.value:
  83.             db = gtk.Button()
  84.             l = gtk.Label()
  85.             l.set_markup_with_mnemonic('<span size="x-small">'+_('_Clear')+'</span>')
  86.             db.add(l); l.show()
  87.             db.connect('clicked',self.number_clicked,0)
  88.             self.table.attach(db,0,side,y+1,y+2)
  89.         self.connect('key-release-event',self.key_press_cb)
  90.         self.show_all()
  91.  
  92.     def key_press_cb (self, w, e):
  93.         txt = gtk.gdk.keyval_name(e.keyval)
  94.         if txt == 'Escape':
  95.             self.emit('changed')
  96.         elif txt in ['0','Delete','BackSpace']:
  97.             self.value = None
  98.             self.emit('changed')
  99.         else:
  100.             try:
  101.                 self.value = int(txt)
  102.             except:
  103.                 print "Can't make sense of %s"%txt
  104.             else:
  105.                 self.emit('changed')
  106.  
  107.     def number_clicked (self, button, n):
  108.         self.value = n
  109.         self.emit('changed')
  110.  
  111.     def get_value (self):
  112.         return self.value
  113.  
  114.     def set_value (self,n):
  115.         self.value = n
  116.  
  117. class NumberBox (gtk.Widget):
  118.  
  119.     text = ''
  120.     top_note_text = ''
  121.     bottom_note_text = ''    
  122.     read_only = False
  123.     read_only_hidden = False
  124.     _layout = None
  125.     _top_note_layout = None
  126.     _bottom_note_layout = None    
  127.     text_color = None
  128.     highlight_color = None
  129.     custom_background_color = None
  130.  
  131.     __gsignals__ = {
  132.         'value-about-to-change':(gobject.SIGNAL_RUN_LAST,gobject.TYPE_NONE,()),
  133.         'changed':(gobject.SIGNAL_RUN_LAST,gobject.TYPE_NONE,()),
  134.         # undo-change - A hacky way to handle the fact that we want to
  135.         # respond to undo's changes but we don't want undo to respond
  136.         # to itself...
  137.         'undo-change':(gobject.SIGNAL_RUN_LAST,gobject.TYPE_NONE,()), 
  138.         'notes-changed':(gobject.SIGNAL_RUN_LAST,gobject.TYPE_NONE,()),
  139.         }
  140.  
  141.     base_state = gtk.STATE_NORMAL
  142.     number_picker_mode = False
  143.     draw_boxes = False
  144.     
  145.     def __init__ (self, upper=9, text=''):
  146.         gtk.Widget.__init__(self)
  147.         self.upper = upper
  148.         self.font = self.style.font_desc
  149.         self.font.set_size(BASE_FONT_SIZE)
  150.         self.note_font = self.font.copy()
  151.         self.note_font.set_size(NOTE_FONT_SIZE)
  152.         self.set_property('can-focus',True)
  153.         self.set_property('events',gtk.gdk.ALL_EVENTS_MASK)
  154.         self.connect('button-press-event',self.button_press_cb)
  155.         self.connect('key-release-event',self.key_press_cb)
  156.         self.connect('enter-notify-event',self.pointer_enter_cb)
  157.         self.connect('leave-notify-event',self.pointer_leave_cb)
  158.         self.connect('focus-in-event',self.focus_in_cb)
  159.         self.connect('focus-out-event',self.focus_out_cb)
  160.         self.connect('motion-notify-event',self.motion_notify_cb)
  161.         self.set_text(text)
  162.  
  163.     def pointer_enter_cb (self, *args):
  164.         if not self.is_focus(): self.set_state(gtk.STATE_PRELIGHT)
  165.     def pointer_leave_cb (self, *args):
  166.         self.set_state(self.base_state)
  167.         self._toggle_box_drawing_(False)
  168.  
  169.     def focus_in_cb (self, *args):
  170.         self.set_state(gtk.STATE_SELECTED)
  171.         self.base_state = gtk.STATE_SELECTED
  172.  
  173.     def focus_out_cb (self, *args):
  174.         self.set_state(gtk.STATE_NORMAL)
  175.         self.base_state = gtk.STATE_NORMAL
  176.         self.number_picker_mode = False
  177.         if hasattr(self,'npicker') and self.npicker:
  178.             self.npicker.destroy()
  179.             self.npicker = None
  180.  
  181.     def motion_notify_cb (self, *args):
  182.         if self.is_focus():
  183.             self._toggle_box_drawing_(True)
  184.         else:
  185.             self._toggle_box_drawing_(False)                        
  186.  
  187.     def _toggle_box_drawing_ (self, val):
  188.         if val and not self.draw_boxes:
  189.             self.draw_boxes = True
  190.             self.queue_draw()
  191.         if (not val) and self.draw_boxes:
  192.             self.draw_boxes = False
  193.             self.queue_draw()                            
  194.  
  195.     def button_press_cb (self, w, e):
  196.         if self.read_only: return
  197.         if e.type == gtk.gdk._2BUTTON_PRESS:
  198.             # ignore second click (this makes a double click in the
  199.             # middle of a cell get us a display of the numbers, rather
  200.             # than selecting a number.
  201.             return
  202.         if self.is_focus():
  203.             x,y = e.get_coords()        
  204.             alloc = self.get_allocation()
  205.             my_w = alloc.width
  206.             my_h = alloc.height
  207.             if self.number_picker_mode:
  208.                 # If we are a number picker...
  209.                 xperc = float(x)/my_w
  210.                 yperc = float(y)/my_h
  211.                 if xperc > 0.75:
  212.                     # If we're in the right quadrant, we get out of number picker mode
  213.                     self.set_text_interactive('')
  214.                     self.number_picker_mode = False
  215.                     self.queue_draw()
  216.                     return
  217.                 else:
  218.                     if xperc > 0.5: xval = 3
  219.                     elif xperc > 0.25: xval = 2
  220.                     else: xval = 1
  221.                     if yperc > 0.66: yval = 6
  222.                     elif yperc > 0.33: yval = 3
  223.                     else: yval = 0
  224.                     self.number_picker_mode = False
  225.                     self.set_text_interactive('')
  226.                     self.set_text_interactive(str(xval+yval))
  227.             else:
  228.                 border_height = float(BORDER_WIDTH)/BASE_SIZE
  229.                 if float(y)/my_h < border_height:
  230.                     self.show_note_editor(top=True)
  231.                 elif float(y)/my_h > (1-border_height):
  232.                     self.show_note_editor(top=False)
  233.                 else:
  234.                     # In this case we're a normal old click...
  235.                     if hasattr(self,'npicker') and self.npicker:
  236.                         self.npicker.destroy()
  237.                         self.npicker = None
  238.                     self.show_number_picker()
  239.         else:
  240.             self.grab_focus()
  241.  
  242.     def key_press_cb (self, w, e):
  243.         if self.read_only: return
  244.         txt = gtk.gdk.keyval_name(e.keyval)
  245.         if type(txt) == type(None):
  246.             # Make sure we don't trigger on unplugging the A/C charger etc
  247.             return        
  248.         txt = txt.replace('KP_', '')
  249.         if self.get_text() == txt:
  250.             # If there's no change, do nothing
  251.             return
  252.         if txt in ['0','Delete','BackSpace']:
  253.             self.set_text_interactive('')
  254.         elif txt in ['n','N']:
  255.             self.show_note_editor(top=True)
  256.         elif txt in ['m','M']:
  257.             self.show_note_editor(top=False)
  258.         # And then add the new value if need be
  259.         elif txt in [str(n) for n in range(1,self.upper+1)]:
  260.             # First do a removal event -- this is something of a
  261.             # kludge, but it works nicely with old code that was based
  262.             # on entries, which also behave this way (they generate 2
  263.             # events for replacing a number with a new number - a
  264.             # removal event and an addition event)
  265.             if self.get_text(): self.set_text_interactive('')
  266.             # Then add
  267.             self.set_text_interactive(txt)
  268.  
  269.     def note_changed_cb (self, w, top=False):
  270.         if top:
  271.             self.set_note_text_interactive(top_text=w.get_text())
  272.         else:
  273.             self.set_note_text_interactive(bottom_text=w.get_text())
  274.  
  275.     def show_note_editor (self, top=True):
  276.         alloc = self.get_allocation()
  277.         w = gtk.Window()
  278.         w.set_property('skip-pager-hint', True)
  279.         w.set_property('skip-taskbar-hint', True)
  280.         w.set_decorated(False)
  281.         w.set_position(gtk.WIN_POS_MOUSE)
  282.         w.set_size_request(alloc.width,alloc.height/2)
  283.         f = gtk.Frame()
  284.         e = gtk.Entry()
  285.         f.add(e)
  286.         if top: e.set_text(self.top_note_text)
  287.         else: e.set_text(self.bottom_note_text)
  288.         w.add(f)
  289.         e.connect('changed', self.note_changed_cb, top)
  290.         e.connect('focus-out-event',lambda e, ev, w: w.destroy(), w)
  291.         e.connect('activate',lambda e, w: w.destroy(), w)
  292.         x,y = self.window.get_origin()
  293.         if top:
  294.             w.move(x,y)
  295.         else:
  296.             w.move(x,y+int(alloc.height*0.6))
  297.         w.show_all()
  298.         e.grab_focus()
  299.  
  300.     def number_changed_cb (self, ns, w):
  301.         w.destroy()
  302.         self.set_text_interactive('')
  303.         newval = ns.get_value()
  304.         if newval:
  305.             self.set_text_interactive(str(newval))
  306.  
  307.     def show_number_picker (self):
  308.         w = gtk.Window(type=gtk.WINDOW_POPUP)
  309.         ns = NumberSelector(upper=self.upper,default=self.get_value())
  310.         ns.connect('changed', self.number_changed_cb, w)
  311.         w.grab_focus()
  312.         w.add(ns)
  313.         r = w.get_allocation()
  314.         my_origin = self.window.get_origin()
  315.         x,y = self.window.get_size()
  316.         popupx,popupy = w.get_size()
  317.         overlapx = popupx-x
  318.         overlapy = popupy-y
  319.         w.move(my_origin[0]-(overlapx/2),my_origin[1]-(overlapy/2))
  320.         w.show()
  321.         self.npicker = w
  322.  
  323.     def set_text_interactive (self,text):
  324.         self.emit('value-about-to-change')
  325.         self.set_text(text)
  326.         self.queue_draw()
  327.         self.emit('changed')        
  328.  
  329.     def set_font (self, font):
  330.         if type(font)==str:
  331.             font = pango.FontDescription(font)
  332.         self.font = font
  333.         if self.text: self.set_text(self.text)
  334.         self.queue_draw()
  335.  
  336.     def set_note_font (self, font):
  337.         if type(font)==str:
  338.             font = pango.FontDescription(font)
  339.         self.note_font = font
  340.         if self.top_note_text or self.bottom_note_text:
  341.             self.set_note_text(self.top_note_text,
  342.                                self.bottom_note_text)
  343.         self.queue_draw()
  344.  
  345.     def set_text (self, text):
  346.         self.text = text
  347.         self._layout = self.create_pango_layout(text)
  348.         self._layout.set_font_description(self.font)
  349.  
  350.     def set_notes (self, notes):
  351.         """Hackish method to allow easy use of Undo API.
  352.  
  353.         Undo API requires a set method that is called with one
  354.         argument (the result of a get method)"""
  355.         self.set_note_text(top_text=notes[0],
  356.                            bottom_text=notes[1])
  357.         self.queue_draw()
  358.  
  359.     def set_note_text (self, top_text=None,bottom_text=None):
  360.         if top_text is not None:
  361.             self.top_note_text = top_text
  362.             self._top_note_layout = self.create_pango_layout(top_text)
  363.             self._top_note_layout.set_font_description(self.note_font)
  364.         if bottom_text is not None:
  365.             self.bottom_note_text = bottom_text
  366.             self._bottom_note_layout = self.create_pango_layout(bottom_text)
  367.             self._bottom_note_layout.set_font_description(self.note_font)
  368.         self.queue_draw()
  369.  
  370.     def set_note_text_interactive (self, *args, **kwargs):
  371.         self.emit('value-about-to-change')
  372.         self.set_note_text(*args,**kwargs)
  373.         self.emit('notes-changed')
  374.     
  375.     def do_realize (self):
  376.         # The do_realize method is responsible for creating GDK (windowing system)
  377.         # resources. In this example we will create a new gdk.Window which we
  378.         # then draw on
  379.  
  380.         # First set an internal flag telling that we're realized
  381.         self.set_flags(self.flags() | gtk.REALIZED)
  382.  
  383.         # Create a new gdk.Window which we can draw on.
  384.         # Also say that we want to receive exposure events by setting
  385.         # the event_mask
  386.         self.window = gtk.gdk.Window(
  387.             self.get_parent_window(),
  388.             width=self.allocation.width,
  389.             height=self.allocation.height,
  390.             window_type=gtk.gdk.WINDOW_CHILD,
  391.             wclass=gtk.gdk.INPUT_OUTPUT,
  392.             event_mask=self.get_events() | gtk.gdk.EXPOSURE_MASK)
  393.  
  394.         # Associate the gdk.Window with ourselves, Gtk+ needs a reference
  395.         # between the widget and the gdk window
  396.         self.window.set_user_data(self)
  397.  
  398.         # Attach the style to the gdk.Window, a style contains colors and
  399.         # GC contextes used for drawing
  400.         self.style.attach(self.window)
  401.  
  402.         # The default color of the background should be what
  403.         # the style (theme engine) tells us.
  404.         self.style.set_background(self.window, gtk.STATE_NORMAL)
  405.         self.window.move_resize(*self.allocation)
  406.  
  407.     def do_unrealize (self):
  408.         # The do_unrealized method is responsible for freeing the GDK resources
  409.  
  410.         # De-associate the window we created in do_realize with ourselves
  411.         self.window.set_user_data(None)
  412.  
  413.     def do_size_request (self, requisition):
  414.         # The do_size_request method Gtk+ is calling on a widget to ask
  415.         # it the widget how large it wishes to be. It's not guaranteed
  416.         # that gtk+ will actually give this size to the widget
  417.  
  418.         # In this case, we say that we want to be as big as the
  419.         # text is, and a square
  420.         width, height = self._layout.get_size()
  421.         if width > height:
  422.             side = width/pango.SCALE
  423.         else:
  424.             side = height/pango.SCALE
  425.         requisition.width = side; requisition.height = side
  426.  
  427.     def do_size_allocate(self, allocation):
  428.         # The do_size_allocate is called by when the actual size is known
  429.         # and the widget is told how much space could actually be allocated
  430.  
  431.         # Save the allocated space
  432.         self.allocation = allocation
  433.         
  434.         # If we're realized, move and resize the window to the
  435.         # requested coordinates/positions
  436.         if self.flags() & gtk.REALIZED:
  437.             self.window.move_resize(*allocation)
  438.  
  439.     def do_expose_event(self, event):
  440.         # The do_expose_event is called when the widget is asked to draw itself
  441.         # Remember that this will be called a lot of times, so it's usually
  442.         # a good idea to write this code as optimized as it can be, don't
  443.         # Create any resources in here.
  444.         x, y, w, h = self.allocation
  445.         cr = self.window.cairo_create()        
  446.         if h<w:
  447.             scale = h/float(BASE_SIZE)
  448.         else:
  449.             scale = w/float(BASE_SIZE)
  450.         cr.scale(scale,scale)
  451.         self.draw_background_color(cr)
  452.         if self.number_picker_mode:
  453.             self.draw_numbers(cr)
  454.             return
  455.         if self.is_focus():
  456.             self.draw_highlight_box(cr)
  457.         self.draw_normal_box(cr)
  458.         self.draw_text(cr)
  459.         if self.draw_boxes and self.is_focus():
  460.             self.draw_note_area_highlight_box(cr)
  461.         
  462.  
  463.     def draw_background_color (self, cr):        
  464.         if self.read_only:
  465.             if self.custom_background_color:
  466.                 r,g,b = self.custom_background_color
  467.                 cr.set_source_rgb(
  468.                     r*0.6,g*0.6,b*0.6
  469.                     )
  470.             else:
  471.                 cr.set_source_color(self.style.base[gtk.STATE_INSENSITIVE])
  472.         elif self.is_focus():
  473.             cr.set_source_color(self.style.base[gtk.STATE_SELECTED])                
  474.         elif self.custom_background_color:
  475.             cr.set_source_rgb(*self.custom_background_color)
  476.         else:
  477.             cr.set_source_color(
  478.                 self.style.base[self.state]
  479.                 )
  480.         cr.rectangle(
  481.             0,0,BASE_SIZE,BASE_SIZE
  482.             )
  483.         cr.fill()
  484.  
  485.     def draw_normal_box (self, cr):
  486.         state = self.state
  487.         if state == gtk.STATE_SELECTED:
  488.             # When the widget is selected, we still want the outer box to look normal
  489.             state = gtk.STATE_NORMAL
  490.         cr.set_source_color(
  491.             self.style.mid[state]
  492.             )
  493.         cr.rectangle(
  494.             NORMAL_LINE_WIDTH*0.5,
  495.             NORMAL_LINE_WIDTH*0.5,
  496.             BASE_SIZE-NORMAL_LINE_WIDTH,
  497.             BASE_SIZE-NORMAL_LINE_WIDTH,
  498.             )
  499.         cr.set_line_width(NORMAL_LINE_WIDTH)
  500.         cr.set_line_join(cairo.LINE_JOIN_ROUND)
  501.         cr.stroke()
  502.         # And now draw a thinner line around the very outside...
  503.         cr.set_source_color(
  504.             self.style.dark[state]
  505.             )
  506.         cr.rectangle(
  507.             NORMAL_LINE_WIDTH*0.25,
  508.             NORMAL_LINE_WIDTH*0.25,
  509.             BASE_SIZE-NORMAL_LINE_WIDTH*0.5,
  510.             BASE_SIZE-NORMAL_LINE_WIDTH*0.5,
  511.             )
  512.         cr.set_line_width(NORMAL_LINE_WIDTH*0.5)
  513.         cr.set_line_join(cairo.LINE_JOIN_MITER)
  514.         cr.stroke()
  515.  
  516.     def draw_highlight_box (self,cr):
  517.         cr.set_source_color(
  518.             self.style.base[gtk.STATE_SELECTED]
  519.             )
  520.         cr.rectangle(
  521.             # left-top
  522.             BORDER_LINE_WIDTH*0.5,
  523.             BORDER_LINE_WIDTH*0.5,
  524.             # bottom-right
  525.             BASE_SIZE-(BORDER_LINE_WIDTH),
  526.             BASE_SIZE-(BORDER_LINE_WIDTH),
  527.             )
  528.         cr.set_line_width(BORDER_LINE_WIDTH)
  529.         cr.set_line_join(cairo.LINE_JOIN_ROUND)
  530.         cr.stroke()
  531.  
  532.     def draw_note_area_highlight_box (self, cr):
  533.         # set up our paint brush...
  534.         cr.set_source_color(
  535.             self.style.mid[self.state]
  536.             )
  537.         cr.set_line_width(NORMAL_LINE_WIDTH)
  538.         cr.set_line_join(cairo.LINE_JOIN_ROUND)
  539.         # top rectangle
  540.         cr.rectangle(NORMAL_LINE_WIDTH*0.5,
  541.                      NORMAL_LINE_WIDTH*0.5,
  542.                      BASE_SIZE-NORMAL_LINE_WIDTH,
  543.                      BORDER_WIDTH-NORMAL_LINE_WIDTH)
  544.         cr.stroke()
  545.         # bottom rectangle
  546.         cr.rectangle(NORMAL_LINE_WIDTH*0.5,#x
  547.                      BASE_SIZE - BORDER_WIDTH-(NORMAL_LINE_WIDTH*0.5),#y
  548.                      BASE_SIZE-NORMAL_LINE_WIDTH,#x2
  549.                      BASE_SIZE-NORMAL_LINE_WIDTH #y2
  550.                      )
  551.         cr.stroke()
  552.     
  553.     def draw_text (self, cr):
  554.         if self.text_color:
  555.             cr.set_source_rgb(*self.text_color)
  556.         elif self.read_only:
  557.             cr.set_source_color(self.style.text[gtk.STATE_NORMAL])
  558.         else:
  559.             cr.set_source_color(self.style.text[self.state])
  560.         # And draw the text in the middle of the allocated space
  561.         if self._layout:
  562.             fontw, fonth = self._layout.get_pixel_size()
  563.             cr.move_to(
  564.                 (BASE_SIZE/2)-(fontw/2),
  565.                 (BASE_SIZE/2) - (fonth/2),
  566.                 )
  567.             cr.update_layout(self._layout)
  568.             cr.show_layout(self._layout)
  569.         cr.set_source_color(self.style.text[self.state])        
  570.         # And draw any note text...
  571.         if self._top_note_layout:
  572.             fontw, fonth = self._top_note_layout.get_pixel_size()
  573.             cr.move_to(
  574.                 NORMAL_LINE_WIDTH,
  575.                 0,
  576.                 )
  577.             cr.update_layout(self._top_note_layout)
  578.             cr.show_layout(self._top_note_layout)
  579.         if self._bottom_note_layout:
  580.             fontw, fonth = self._bottom_note_layout.get_pixel_size()
  581.             cr.move_to(
  582.                 NORMAL_LINE_WIDTH,
  583.                 BASE_SIZE-fonth,
  584.                 )
  585.             cr.update_layout(self._bottom_note_layout)
  586.             cr.show_layout(self._bottom_note_layout)
  587.  
  588.     def draw_numbers (self, cr):        
  589.         if not hasattr(self,'number_text'):
  590.             self.small_digit_height = 1
  591.             self.small_digit_width = 1
  592.             self.number_text = []
  593.             for n in range(self.upper): # + ['X']:
  594.                 if type(n)==int:
  595.                     n = str(n+1)
  596.                 txt = self.create_pango_layout(n)
  597.                 txt.set_font_description(self.note_font)
  598.                 if not hasattr(self,'bold_note_font'):
  599.                     self.bold_note_font = self.note_font.copy()
  600.                     self.bold_note_font.set_weight(pango.WEIGHT_BOLD)
  601.                 bold_txt = self.create_pango_layout(n)
  602.                 bold_txt.set_font_description(self.bold_note_font)
  603.                 self.number_text.append((txt,bold_txt))
  604.                 w,h = bold_txt.get_pixel_size()
  605.                 if w > self.small_digit_width: self.small_digit_width = w*1.2
  606.                 if h > self.small_digit_height: self.small_digit_height = h
  607.         val = self.get_value()
  608.         cols = (BASE_SIZE-NORMAL_LINE_WIDTH*2) / self.small_digit_width
  609.         rows = (BASE_SIZE-NORMAL_LINE_WIDTH*2) / self.small_digit_height
  610.         cols = 3; rows=3
  611.         row_size = BASE_SIZE/rows
  612.         col_size = BASE_SIZE/cols
  613.         n = 0
  614.         for y in range(rows):
  615.             for x in range(cols):
  616.                 if y < 2 and x > 2: continue
  617.                 if n >= len(self.number_text):
  618.                     break
  619.                 txt = self.number_text[n]
  620.                 if val==(n+1):
  621.                     layout = txt[1] # grab bold layout
  622.                 else:
  623.                     layout = txt[0] # grab normal layout
  624.                 w,h = layout.get_pixel_size()
  625.                 xpadding = (col_size - w)/2
  626.                 ypadding = (row_size - h)/2
  627.                 # draw little boxes...
  628.                 cr.set_source_color(gtk.gdk.Color(2**16,0,0))
  629.                 cr.set_line_width(LITTLE_LINE_WIDTH)
  630.                 cr.rectangle(LITTLE_LINE_WIDTH*0.5+NORMAL_LINE_WIDTH*0.5+(x*col_size),
  631.                              LITTLE_LINE_WIDTH*0.5+NORMAL_LINE_WIDTH*0.5+(y*row_size),
  632.                              LITTLE_LINE_WIDTH*0.5+NORMAL_LINE_WIDTH*0.5+((x+1)*col_size),
  633.                              LITTLE_LINE_WIDTH*0.5+NORMAL_LINE_WIDTH*0.5+((y+1)*row_size),
  634.                              )
  635.                 cr.stroke()
  636.                 cr.set_source_color(self.style.text[self.state])
  637.                 cr.move_to(NORMAL_LINE_WIDTH+(x*col_size)+xpadding,
  638.                              NORMAL_LINE_WIDTH+(y*row_size)+ypadding)
  639.                 cr.update_layout(layout)
  640.                 cr.show_layout(layout)
  641.                 n+=1
  642.  
  643.     def set_text_color (self, color):
  644.         self.text_color = color
  645.         self.queue_draw()
  646.  
  647.     def set_background_color (self, color):
  648.         self.custom_background_color = color
  649.         self.queue_draw()
  650.  
  651.     def hide_notes (self):
  652.         pass
  653.  
  654.     def show_notes (self):
  655.         pass
  656.  
  657.     def set_value_from_undo (self, v):
  658.         self.set_value(v)
  659.         self.emit('undo_change')
  660.  
  661.     def set_value (self,v):
  662.         if 0 < v <= self.upper:
  663.             self.set_text(str(v))
  664.         else:
  665.             self.set_text('')
  666.         self.queue_draw()
  667.  
  668.     def get_value (self):
  669.         try: return int(self.text)
  670.         except: return None
  671.  
  672.     def get_text (self): return self.text
  673.     def get_note_text (self): return self.top_note_text,self.bottom_note_text
  674.  
  675. class SudokuNumberBox (NumberBox):
  676.  
  677.     normal_color = None
  678.     highlight_color = ERROR_HIGHLIGHT_COLOR
  679.  
  680.     def set_color (self, color):
  681.         self.normal_color = color
  682.         self.set_text_color(self.normal_color)
  683.  
  684.     def unset_color (self): self.set_color(None)
  685.  
  686.     def set_error_highlight (self, val):
  687.         if val:
  688.             self.set_text_color((1.0,0,0))
  689.         else:
  690.             self.set_text_color(self.normal_color)
  691.  
  692.     def set_read_only (self, val):
  693.         self.read_only = val
  694.         if not hasattr(self,'bold_font'):
  695.             self.normal_font = self.font
  696.             self.bold_font = self.font.copy()
  697.             self.bold_font.set_weight(pango.WEIGHT_BOLD)
  698.         if self.read_only:
  699.             self.set_font(self.bold_font)
  700.         else:
  701.             self.set_font(self.normal_font)
  702.         self.queue_draw()
  703.  
  704.     def set_impossible (self, val):
  705.         if val: self.set_text('X')
  706.         else: self.set_text('')
  707.  
  708.     
  709. gobject.type_register(NumberBox)
  710.  
  711. class SudokuNumberGrid (gtk.AspectFrame):
  712.  
  713.     def __init__ (self, group_size=9):
  714.         self.table = gtk.Table(rows=group_size,columns=group_size,homogeneous=True)
  715.         self.group_size = group_size        
  716.         self.__entries__ = {}
  717.         for x in range(self.group_size):
  718.             for y in range(self.group_size):
  719.                 e = SudokuNumberBox(upper=self.group_size)
  720.                 e.x = x
  721.                 e.y = y
  722.                 self.table.attach(e,x,x+1,y,y+1,
  723.                                   )
  724.                 self.__entries__[(x,y)] = e
  725.         gtk.AspectFrame.__init__(self,obey_child=False)
  726.         self.set_shadow_type(gtk.SHADOW_NONE)
  727.         self.eb = gtk.EventBox()
  728.         self.eb.add(self.table)
  729.         self.add(self.eb)
  730.         self.connect('size-allocate',self.allocate_cb)
  731.         self.show_all()
  732.  
  733.     def allocate_cb (self, w, rect):
  734.         if rect.width > rect.height: side = rect.height
  735.         else: side = rect.width
  736.         # we want our small spacing to be 1/15th the size of a box
  737.         spacing = float(side) / (self.group_size * SPACING_FACTOR)
  738.         if spacing == 0: spacing = 1
  739.         if hasattr(self,'small_spacing') and spacing == self.small_spacing:
  740.             return
  741.         else:
  742.             self.change_spacing(spacing)
  743.  
  744.     def change_spacing (self, small_spacing):
  745.         self.small_spacing = small_spacing
  746.         self.big_spacing = int(small_spacing*SMALL_TO_BIG_FACTOR)
  747.         self.table.set_row_spacings(int(small_spacing))
  748.         self.table.set_col_spacings(int(small_spacing))
  749.         box_side = int(math.sqrt(self.group_size))
  750.         for n in range(1,box_side):
  751.             self.table.set_row_spacing(box_side*n-1,self.big_spacing)
  752.             self.table.set_col_spacing(box_side*n-1,self.big_spacing)
  753.         self.table.set_border_width(self.big_spacing)
  754.         
  755.     def get_focused_entry (self):
  756.         for e in self.__entries__.values():
  757.             if e.is_focus():
  758.                 return e
  759.  
  760.     def set_bg_color (self, color):
  761.         if type(color)==str:
  762.             try: color = gtk.gdk.color_parse(color)
  763.             except:
  764.                 print 'set_bg_color handed Bad color',color
  765.                 return
  766.         self.eb.modify_bg(gtk.STATE_NORMAL,color)
  767.         self.eb.modify_base(gtk.STATE_NORMAL,color)
  768.         self.eb.modify_fg(gtk.STATE_NORMAL,color)
  769.         self.table.modify_bg(gtk.STATE_NORMAL,color)
  770.         self.table.modify_base(gtk.STATE_NORMAL,color)
  771.         self.table.modify_fg(gtk.STATE_NORMAL,color)
  772.         for e in self.__entries__.values():
  773.             e.modify_bg(gtk.STATE_NORMAL,color)
  774.  
  775. class ParallelDict (dict):
  776.     """A handy new sort of dictionary for tracking conflicts.
  777.  
  778.     pd = ParallelDict()
  779.     pd[1] = [2,3,4] # 1 is linked with 2,3 and 4
  780.     pd -> {1:[2,3,4],2:[1],3:[1],4:[1]}
  781.     pd[2] = [1,3,4] # 2 is linked with 3 and 4 as well as 1
  782.     pd -> {1: [2,3,4],2:[3,4],3:[1,2],4:[1,2]}
  783.     Now for the cool part...
  784.     del pd[1]
  785.     pd -> {2: [2,3],3:[2],4:[2]}
  786.     
  787.     Pretty neat, no?
  788.     """
  789.     def __init__ (self, *args):
  790.         dict.__init__(self,*args)
  791.  
  792.     def __setitem__ (self, k, v):
  793.         dict.__setitem__(self,k,set(v))
  794.         for i in v:
  795.             if i == k: continue
  796.             if self.has_key(i):
  797.                 self[i].add(k)
  798.             else:
  799.                 dict.__setitem__(self,i,set([k]))
  800.  
  801.     def __delitem__ (self, k):
  802.         v=self[k]
  803.         dict.__delitem__(self,k)
  804.         for i in v:
  805.             if i==k: continue
  806.             if self.has_key(i):
  807.                 # Make sure we have a reference to i. If we don't
  808.                 # something has gone wrong... but according to bug
  809.                 # 385937 this has gone wrong at least once, so we'd
  810.                 # better check for it.
  811.                 if k in self[i]: self[i].remove(k)
  812.                 if not self[i]:
  813.                     # If k was the last value in the list of values
  814.                     # for i, then we delete i from our dictionary
  815.                     dict.__delitem__(self,i)
  816.  
  817. class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
  818.  
  819.     __gsignals__ = {
  820.         'focus-changed':(gobject.SIGNAL_RUN_LAST,gobject.TYPE_NONE,()),
  821.         'puzzle-finished':(gobject.SIGNAL_RUN_LAST,gobject.TYPE_NONE,())
  822.         }
  823.  
  824.     do_highlight_cells = False
  825.     
  826.     @simple_debug
  827.     def __init__ (self,grid=None,group_size=9,
  828.                   show_impossible_implications=False):
  829.         group_size=int(group_size)
  830.         self.hints = 0
  831.         self.always_show_hints = False
  832.         self.auto_fills = 0
  833.         self.show_impossible_implications = show_impossible_implications
  834.         self.impossible_hints = 0
  835.         self.impossibilities = []
  836.         self.trackers = {}
  837.         self.__trackers_tracking__ = {}
  838.         self.__colors_used__ = [None,ERROR_HIGHLIGHT_COLOR]
  839.         gobject.GObject.__init__(self)
  840.         SudokuNumberGrid.__init__(self,group_size=group_size)
  841.         self.setup_grid(grid,group_size)
  842.         for e in self.__entries__.values():
  843.             e.show()
  844.             e.connect('undo-change',self.entry_callback)
  845.             e.connect('changed',self.entry_callback)
  846.             e.connect('focus-in-event',self.focus_callback)
  847.         self.connect('focus-changed',self.highlight_cells)
  848.  
  849.     @simple_debug
  850.     def focus_callback (self, e, event):
  851.         self.focused = e
  852.         self.emit('focus-changed')
  853.  
  854.     def get_highlight_colors (self):
  855.         entry = self.__entries__.values()[0]
  856.         default_color = gtkcolor_to_rgb(entry.style.bg[gtk.STATE_SELECTED])
  857.         hsv = colors.rgb_to_hsv(*default_color)
  858.         box_s = hsv[1]
  859.         box_v = hsv[2]
  860.         if box_v < 0.5: box_v = box_v*2
  861.         if box_s > 0.75:
  862.             box_s = box_s*0.5
  863.         else:
  864.             box_s = box_s*1.5
  865.             if box_s > 1: box_s = 1.0
  866.         self.box_color = colors.hsv_to_rgb(
  867.             hsv[0],box_s,box_v
  868.             )
  869.         self.box_and_row_color = colors.rotate_hue_rgb(*self.box_color,**{'rotate_by':0.33/2})
  870.         self.row_color = colors.rotate_hue_rgb(*self.box_color,**{'rotate_by':0.33})
  871.         self.col_color = colors.rotate_hue_rgb(*self.box_color,**{'rotate_by':0.66})
  872.         self.box_and_col_color = colors.rotate_hue_rgb(*self.box_color,**{'rotate_by':1.0-(0.33/2)})
  873.  
  874.     def toggle_highlight (self, val):
  875.         self.do_highlight_cells = val
  876.         self.unhighlight_cells()
  877.         if hasattr(self,'focused') and self.focused: self.highlight_cells()
  878.  
  879.     def unhighlight_cells (self, *args):
  880.         for e in self.__entries__.values(): e.set_background_color(None)
  881.  
  882.     def highlight_cells (self, *args):
  883.         if not self.do_highlight_cells: return
  884.         self.unhighlight_cells()
  885.         if not hasattr(self,'box_color'): self.get_highlight_colors()
  886.         my_x,my_y = self.focused.x,self.focused.y        
  887.  
  888.         # col_coords can sometimes be null.
  889.         if not hasattr(self.grid, 'col_coords'): return
  890.  
  891.         for x,y in self.grid.col_coords[my_x]:
  892.             if (x,y) != (my_x,my_y):
  893.                 self.__entries__[(x,y)].set_background_color(self.col_color)
  894.         for x,y in self.grid.row_coords[my_y]:
  895.             if (x,y) != (my_x,my_y):
  896.                 self.__entries__[(x,y)].set_background_color(self.row_color)
  897.         for x,y in self.grid.box_coords[self.grid.box_by_coords[(my_x,my_y)]]:
  898.             if (x,y) != (my_x,my_y):
  899.                 e = self.__entries__[(x,y)]
  900.                 if x==my_x:
  901.                     e.set_background_color(self.box_and_col_color)
  902.                 elif y==my_y:
  903.                     e.set_background_color(self.box_and_row_color)
  904.                 else:
  905.                     e.set_background_color(self.box_color)
  906.         
  907.  
  908.     @simple_debug
  909.     def show_hint (self):        
  910.         if hasattr(self,'focused'):
  911.             entry = self.focused
  912.             if entry.read_only or entry.get_text():
  913.                 pass
  914.             else:
  915.                 self.show_hint_for_entry(entry,interactive=True)
  916.  
  917.     def show_hint_for_entry (self, entry, interactive=False):
  918.         if interactive:
  919.             set_method = entry.set_note_text_interactive
  920.         else:
  921.             set_method = entry.set_note_text
  922.         vals=self.grid.possible_values(entry.x,entry.y)
  923.         vals = list(vals)
  924.         vals.sort()
  925.         if vals:
  926.             ''.join([str(v) for v in vals])
  927.             txt = ''.join([str(v) for v in vals])
  928.             if txt != entry.get_text():
  929.                 set_method(bottom_text=txt)
  930.                 self.hints += 1
  931.         elif not entry.get_text():
  932.             if entry.get_text() != 'X':
  933.                 self.hints += 1
  934.                 set_method(bottom_text='X')
  935.         else:
  936.             set_method(bottom_text="")
  937.  
  938.     @simple_debug
  939.     def num_to_str (self, n):
  940.         if n >= 10: return SuperNumberEntry.conversions[n]
  941.         else: return str(n)
  942.  
  943.     @simple_debug
  944.     def reset_grid (self):
  945.         """Reset grid to its original setup.
  946.  
  947.         Return a list of items we removed so that callers can handle
  948.         e.g. Undo properly"""
  949.         removed = []
  950.         for x in range(self.group_size):
  951.             for y in range(self.group_size):
  952.                 if not self.grid.virgin._get_(x,y):
  953.                     val = self.__entries__[(x,y)].get_value() # get the value from the user-visible grid, 
  954.                     if val:
  955.                         removed.append((x,y,val,self.trackers_for_point(x,y,val)))
  956.                         self.remove(x,y,do_removal=True)
  957.         return removed
  958.  
  959.     def clear_notes (self, clear_args={'top_text':'','bottom_text':''}):
  960.         """Remove all notes."""
  961.         self.removed = []
  962.         for x in range(self.group_size):
  963.             for y in range(self.group_size):
  964.                 e = self.__entries__[(x,y)]
  965.                 top,bottom = e.get_note_text()
  966.                 if top or bottom:
  967.                     self.removed.append((x,y,(top,bottom)))
  968.                     e.set_note_text(**clear_args)
  969.                     e.queue_draw()
  970.         return self.removed
  971.  
  972.     def clear_hints (self):
  973.         self.clear_notes(clear_args={'bottom_text':''})
  974.     
  975.     @simple_debug
  976.     def blank_grid (self):
  977.         for x in range(self.group_size):
  978.             for y in range(self.group_size):
  979.                 self.remove(x,y)
  980.                 e=self.__entries__[(x,y)]
  981.                 e.set_read_only(False)
  982.         self.grid = None
  983.         self.clear_notes()
  984.  
  985.     @simple_debug
  986.     def change_grid (self, grid, group_size):
  987.         self.auto_fills = 0
  988.         self.hints = 0
  989.         self.impossible_hints = 0
  990.         self.trackers = {}
  991.         self.__trackers_tracking__ = {}
  992.         self.__colors_used__ = [None,ERROR_HIGHLIGHT_COLOR]
  993.         self.blank_grid()
  994.         self.setup_grid(grid,group_size)        
  995.  
  996.     @simple_debug
  997.     def load_game (self, game):
  998.         """Load a game.
  999.  
  1000.         A game is simply a two lined string where the first line represents our
  1001.         virgin self and line two represents our game-in-progress.
  1002.         """
  1003.         self.blank_grid()
  1004.         if '\n' in game:
  1005.             virgin,in_prog = game.split('\n')
  1006.         else:
  1007.             virgin = game; in_prog = ''
  1008.         group_size=int(math.sqrt(len(virgin.split())))
  1009.         self.change_grid(virgin,group_size=group_size)
  1010.         # This int() will break if we go to 16x16 grids...
  1011.         if in_prog:
  1012.             values = [int(c) for c in in_prog.split()]
  1013.             for row in range(group_size):
  1014.                 for col in range(group_size):
  1015.                     index = row * 9 + col
  1016.                     if values[index] and not self.grid._get_(col,row):
  1017.                         self.add_value_to_ui(col,row,values[index])
  1018.  
  1019.     @simple_debug
  1020.     def setup_grid (self, grid, group_size):
  1021.         self.doing_initial_setup = True
  1022.         self.__error_pairs__ = ParallelDict()
  1023.         if isinstance(grid,sudoku.SudokuGrid):
  1024.             self.grid = sudoku.InteractiveSudoku(grid.grid,group_size=grid.group_size)
  1025.         else:
  1026.             self.grid = sudoku.InteractiveSudoku(grid,group_size=group_size)
  1027.         for x in range(group_size):
  1028.             for y in range(group_size):
  1029.                 val=self.grid._get_(x,y)
  1030.                 if val: self.add_value(x,y,val)
  1031.         self.doing_initial_setup = False
  1032.  
  1033.     @simple_debug
  1034.     def entry_callback (self, widget, *args):        
  1035.         if not widget.get_text():
  1036.             if self.grid and self.grid._get_(widget.x,widget.y):
  1037.                 self.grid.remove(widget.x,widget.y)
  1038.             self.remove(widget.x,widget.y)
  1039.         else:
  1040.             self.entry_validate(widget)
  1041.         if self.show_impossible_implications:
  1042.             self.mark_impossible_implications(widget.x,widget.y)
  1043.         if self.always_show_hints:
  1044.             self.update_all_hints()
  1045.  
  1046.     def update_all_hints (self):
  1047.         for x in range(self.group_size):
  1048.             for y in range(self.group_size):
  1049.                 e = self.__entries__[(x,y)]
  1050.                 if e.read_only:
  1051.                     pass
  1052.                 elif e.get_text():
  1053.                     e.set_note_text(bottom_text='')
  1054.                 else:
  1055.                     self.show_hint_for_entry(e)
  1056.  
  1057.     @simple_debug
  1058.     def entry_validate (self, widget, *args):
  1059.         val = widget.get_value()
  1060.         try:
  1061.             self.add_value(widget.x,widget.y,val)
  1062.             if self.grid.check_for_completeness():
  1063.                 self.emit('puzzle-finished')
  1064.         except sudoku.ConflictError, err:
  1065.             conflicts=self.grid.find_conflicts(err.x,err.y,err.value)
  1066.             for conflict in conflicts:
  1067.                 widget.set_error_highlight(True)
  1068.                 self.__entries__[conflict].set_error_highlight(True)
  1069.             self.__error_pairs__[(err.x,err.y)]=conflicts
  1070.  
  1071.     def add_value_to_ui (self, x, y, val, trackers=[]):
  1072.         """Add our value back to our grid come hell or high water.
  1073.  
  1074.         We add our value -- if there is an error, we make the value
  1075.         appear as if the user had typed it; i.e. it will show up with
  1076.         error highlighting."""
  1077.         try:
  1078.             self.add_value(x, y, val, trackers=[])
  1079.         except sudoku.ConflictError:
  1080.             self.entry_validate(self.__entries__[(x,y)])
  1081.  
  1082.     @simple_debug
  1083.     def add_value (self, x, y, val, trackers=[]):
  1084.         """Add value val at position x,y.
  1085.  
  1086.         If tracker is True, we track it with tracker ID tracker.
  1087.  
  1088.         Otherwise, we use any currently tracking trackers to track our addition.
  1089.  
  1090.         Providing the tracker arg is mostly useful for e.g. undo/redo
  1091.         or removed items.
  1092.  
  1093.         To specify NO trackers, use trackers=[-1]
  1094.         """
  1095.         # Add the value to the UI to display
  1096.         self.__entries__[(x,y)].set_value(val)
  1097.         if self.doing_initial_setup:
  1098.             self.__entries__[(x,y)].set_read_only(True)
  1099.         # Handle any trackers.
  1100.         if trackers:
  1101.             # Explicitly specified tracker
  1102.             for tracker in trackers:
  1103.                 if tracker==-1: pass
  1104.                 self.__entries__[(x,y)].set_color(self.get_tracker_color(tracker))
  1105.                 self.trackers[tracker].append((x,y,val))
  1106.         elif True in self.__trackers_tracking__.values():        
  1107.             for k,v in self.__trackers_tracking__.items():
  1108.                 if v:
  1109.                     self.__entries__[(x,y)].set_color(self.get_tracker_color(k))
  1110.                     self.trackers[k].append((x,y,val))
  1111.         # Add value to our underlying sudoku grid -- this will raise
  1112.         # an error if the value is out of bounds with the current
  1113.         # rules. 
  1114.         self.grid.add(x,y,val,True)
  1115.         # Draw our entry
  1116.         self.__entries__[(x,y)].queue_draw()
  1117.  
  1118.     @simple_debug
  1119.     def remove (self, x, y, do_removal=False):
  1120.         """Remove x,y from our visible grid.
  1121.  
  1122.         If do_removal, remove it from our underlying grid as well.
  1123.         """        
  1124.         e=self.__entries__[(x,y)]
  1125.         if do_removal and self.grid and self.grid._get_(x,y):
  1126.             self.grid.remove(x,y)
  1127.         if self.__error_pairs__.has_key((x,y)):
  1128.             e.set_error_highlight(False)
  1129.             errors_removed = self.__error_pairs__[(x,y)]
  1130.             del self.__error_pairs__[(x,y)]
  1131.             for coord in errors_removed:
  1132.                 # If we're not an error by some other pairing...
  1133.                 if not self.__error_pairs__.has_key(coord):
  1134.                     linked_entry = self.__entries__[coord]
  1135.                     linked_entry.set_error_highlight(False)
  1136.                     # Its possible this highlighted error was never
  1137.                     # added to our internal grid, in which case we'd
  1138.                     # better make sure it is...
  1139.                     if self.grid and not self.grid._get_(linked_entry.x,linked_entry.y):
  1140.                         # entry_validate will add the value to our
  1141.                         # internal grid if there are no other
  1142.                         # conflicts
  1143.                         self.entry_validate(linked_entry)
  1144.         # remove trackers
  1145.         for t in self.trackers_for_point(x,y):
  1146.             remove = []
  1147.             for crumb in self.trackers[t]:
  1148.                 if crumb[0]==x and crumb[1]==y:
  1149.                     remove.append(crumb)
  1150.             for r in remove:
  1151.                 self.trackers[t].remove(r)
  1152.         if e.get_text(): e.set_value(0)
  1153.         e.unset_color()
  1154.  
  1155.     @simple_debug
  1156.     def auto_fill (self):
  1157.         changed=self.grid.auto_fill()
  1158.         retval = []
  1159.         for coords,val in changed:
  1160.             self.add_value(coords[0],coords[1],val)
  1161.             retval.append((coords[0],coords[1],val))
  1162.             if self.show_impossible_implications:
  1163.                 self.mark_impossible_implications(*coords)
  1164.         if retval: self.auto_fills += 1
  1165.         if self.grid.check_for_completeness():
  1166.             self.emit('puzzle-finished')
  1167.         return retval
  1168.  
  1169.     @simple_debug
  1170.     def auto_fill_current_entry (self):
  1171.         e = self.get_focused_entry()
  1172.         if not e: return
  1173.     filled = self.grid.auto_fill_for_xy(e.x,e.y)
  1174.         if filled and filled!=-1:
  1175.             e.set_text_interactive('')
  1176.             e.set_text_interactive(str(filled[1]))
  1177.     
  1178.     @simple_debug
  1179.     def mark_impossible_implications (self, x, y):
  1180.         if not self.grid: return
  1181.         implications = self.grid.find_impossible_implications(x,y)
  1182.         if implications:
  1183.             for x,y in implications:
  1184.                 self.__entries__[(x,y)].set_impossible(True)
  1185.                 if not (x,y) in self.impossibilities:
  1186.                     self.impossible_hints += 1
  1187.         for x,y in self.impossibilities:
  1188.             if not (x,y) in implications:
  1189.                 self.__entries__[(x,y)].set_impossible(False)
  1190.         self.impossibilities = implications
  1191.  
  1192.     @simple_debug
  1193.     def create_tracker (self, identifier=0):
  1194.         if not identifier: identifier = 0
  1195.         while self.trackers.has_key(identifier): identifier+=1
  1196.         self.trackers[identifier]=[]
  1197.         return identifier
  1198.  
  1199.     def trackers_for_point (self, x, y, val=None):
  1200.         if val:
  1201.             # if we have a value we can do this a simpler way...
  1202.             track_for_point = filter(
  1203.                 lambda t: (x,y,val) in t[1],
  1204.                 self.trackers.items()
  1205.                 )
  1206.         else:
  1207.             track_for_point = filter(
  1208.                 lambda tkr: True in [t[0]==x and t[1]==y for t in tkr[1]],
  1209.                 self.trackers.items())
  1210.         return [t[0] for t in track_for_point]
  1211.  
  1212.     def get_tracker_color (self, identifier):
  1213.         if len(TRACKER_COLORS)>identifier:
  1214.             return TRACKER_COLORS[identifier]
  1215.         else:
  1216.             random_color = TRACKER_COLORS[0]
  1217.             while random_color in TRACKER_COLORS:
  1218.                 # If we have generated all possible colors, this will
  1219.                 # enter an infinite loop
  1220.                 random_color = (random.randint(0,100)/100.0,
  1221.                                 random.randint(0,100)/100.0,
  1222.                                 random.randint(0,100)/100.0)
  1223.             TRACKER_COLORS.append(random_color)
  1224.             return self.get_tracker_color(identifier)
  1225.  
  1226.     @simple_debug
  1227.     def toggle_tracker (self, identifier, value):
  1228.         """Toggle tracking for tracker identified by identifier."""
  1229.         self.__trackers_tracking__[identifier]=value
  1230.  
  1231.     def delete_by_tracker (self, identifier):
  1232.         """Delete all cells tracked by tracker ID identifer."""
  1233.         ret = []
  1234.         while self.trackers[identifier]:
  1235.             x,y,v = self.trackers[identifier][0]
  1236.             ret.append((x,y,v,self.trackers_for_point(x,y,v)))
  1237.             self.remove(x,y)
  1238.             if self.grid and self.grid._get_(x,y):
  1239.                 self.grid.remove(x,y)
  1240.         return ret
  1241.  
  1242.     def delete_except_for_tracker (self, identifier):
  1243.         tracks = self.trackers[identifier]
  1244.         removed = []
  1245.         for x in range(self.group_size):
  1246.             for y in range(self.group_size):
  1247.                 val = self.grid._get_(x,y)
  1248.                 if (val
  1249.                     and (x,y,val) not in tracks 
  1250.                     and not self.grid.virgin._get_(x,y)
  1251.                     ):
  1252.                     removed.append((x,y,val,self.trackers_for_point(x,y,val)))
  1253.                     self.remove(x,y)
  1254.                     if self.grid and self.grid._get_(x,y):
  1255.                         self.grid.remove(x,y)
  1256.  
  1257.         return removed
  1258.  
  1259.     def add_tracker (self, x, y, tracker, val=None):
  1260.         self.__entries__[(x,y)].set_color(self.get_tracker_color(tracker))
  1261.         if not val: val = self.grid._get_(x,y)
  1262.         self.trackers[tracker].append((x,y,val))
  1263.  
  1264.     def remove_tracker (self, x, y, tracker, val=None):
  1265.         if not val: val = self.grid._get_(x,y)
  1266.         self.trackers[tracker].remove((x,y,val))
  1267.  
  1268. class GridDancer:
  1269.  
  1270.     DANCE_COLORS = [colors.color_hex_to_float(hx) for hx in 
  1271.                     [
  1272.         '#cc0000', # red
  1273.         '#ef2929',
  1274.         '#f57900', # orange
  1275.         '#fcaf3e',
  1276.         '#fce94f',
  1277.         '#8ae234', # green
  1278.         '#73d216',
  1279.         '#729fcf', # blue
  1280.         '#3465a4',
  1281.         '#ad7fa8', # violet
  1282.         '#75507b', ]
  1283.                     ]
  1284.  
  1285.     STEPS_PER_ANIMATION = 10
  1286.     
  1287.     def __init__ (self, grid):
  1288.         self.animations = [self.value_dance,
  1289.                            self.box_dance,
  1290.                            self.col_dance,
  1291.                            self.row_dance,]
  1292.         self.current_animation = self.value_dance
  1293.         self.step = 0
  1294.         self.grid = grid
  1295.         self.dancing = False
  1296.         for box in self.grid.__entries__.values():
  1297.             if box.read_only == True:
  1298.                 box.read_only = False
  1299.                 box.read_only_hidden = True
  1300.             box.queue_draw()
  1301.  
  1302.     def start_dancing (self):
  1303.         for box in self.grid.__entries__.values():
  1304.             box.props.can_focus = False
  1305.         self.grid.get_toplevel().child_focus(gtk.DIR_TAB_BACKWARD)
  1306.         self.dancing = True
  1307.         gobject.timeout_add(350,self.dance_grid)
  1308.  
  1309.     def stop_dancing (self):
  1310.         self.dancing = False
  1311.         for box in self.grid.__entries__.values():
  1312.             box.props.can_focus = True
  1313.             if box.read_only_hidden == True:
  1314.                 box.read_only = True
  1315.                 box.read_only_hidden = False
  1316.         self.grid.unhighlight_cells()
  1317.  
  1318.     def do_dance_step (self):
  1319.         self.grid.__entries__[(random.randint(0,8),
  1320.                                random.randint(0,8))
  1321.                               ].set_background_color(
  1322.             random.choice(self.DANCE_COLORS)
  1323.             )
  1324.     
  1325.     def rotate_animation (self):
  1326.         ci = self.animations.index(self.current_animation)
  1327.         if (ci+1) == len(self.animations):
  1328.             ni = 0
  1329.         else:
  1330.             ni = ci + 1
  1331.         self.current_animation = self.animations[ni]
  1332.         self.step = 0
  1333.  
  1334.     def dance_grid (self):
  1335.         if not self.dancing: return
  1336.         if self.step > self.STEPS_PER_ANIMATION:
  1337.             self.rotate_animation()
  1338.         if hasattr(self,'adjustment'):
  1339.             self.adjustment += 1
  1340.         else:
  1341.             self.adjustment = 0
  1342.         if self.adjustment >= 9: self.adjustment = 0
  1343.         try:
  1344.             self.current_animation()
  1345.         except AttributeError:
  1346.             return True
  1347.         self.step += 1
  1348.         if self.dancing:
  1349.             return True
  1350.  
  1351.     def col_dance (self):
  1352.         for x in range(9):
  1353.             n = (x + self.adjustment) % len(self.DANCE_COLORS)
  1354.             color = self.DANCE_COLORS[n]
  1355.             for y in range(9):
  1356.                 self.grid.__entries__[(x,y)].set_background_color(color)
  1357.                 
  1358.     def row_dance (self):
  1359.         for y in range(9):
  1360.             n = (y + self.adjustment) % len(self.DANCE_COLORS)
  1361.             color = self.DANCE_COLORS[n]
  1362.             for x in range(9):
  1363.                 self.grid.__entries__[(x,y)].set_background_color(color)
  1364.  
  1365.     def box_dance (self):
  1366.         for box in range(9):
  1367.             n = (box + self.adjustment) % len(self.DANCE_COLORS)
  1368.             color = self.DANCE_COLORS[n]
  1369.             for x,y in self.grid.grid.box_coords[box]:
  1370.                 self.grid.__entries__[(x,y)].set_background_color(color)
  1371.         
  1372.     def value_dance (self):
  1373.         for value in range(10):
  1374.             n = (value + self.adjustment) % len(self.DANCE_COLORS)
  1375.             color = self.DANCE_COLORS[n]
  1376.             for x in range(9):
  1377.                 for y in range(9):
  1378.                     if self.grid.grid._get_(x,y)==value:
  1379.                         self.grid.__entries__[(x,y)].set_background_color(color)
  1380.  
  1381. def test_dance_grid (grid):
  1382.     dancer = GridDancer(grid)
  1383.     dancer.start_dancing()
  1384.     def stop (*args): dancer.stop_dancing()
  1385.     gobject.timeout_add(15000,stop)
  1386.  
  1387. if __name__ == '__main__':
  1388.     def test_sng ():
  1389.         w = gtk.Window()
  1390.         w.connect('delete-event', gtk.main_quit)    
  1391.         t = SudokuNumberGrid(4)
  1392.         w.add(t)
  1393.         t.__entries__[(0,1)].set_color((0.0,1.0,0.0))
  1394.         t.__entries__[(0,1)].set_value(4)
  1395.         t.__entries__[(1,1)].set_error_highlight(True)
  1396.         t.__entries__[(1,1)].set_value(1)
  1397.         t.__entries__[(2,1)].set_color((0.0,0.0,1.0))
  1398.         t.__entries__[(2,1)].set_error_highlight(True)
  1399.         t.__entries__[(2,1)].set_value(2)
  1400.         t.__entries__[(3,1)].set_color((0.0,0.0,1.0))
  1401.         t.__entries__[(3,1)].set_error_highlight(True)
  1402.         t.__entries__[(3,1)].set_error_highlight(False)
  1403.         t.__entries__[(3,1)].set_value(3)
  1404.         t.__entries__[(3,1)].set_note_text('2,3,4','1,2')
  1405.         w.show_all()
  1406.         gtk.main()
  1407.  
  1408.     def reproduce_foobared_rendering ():
  1409.         from sudoku import SudokuGrid, sample_open_sudoku
  1410.         from dialog_swallower import SwappableArea
  1411.         sgd = SudokuGameDisplay()
  1412.         sgd.set_bg_color('black')
  1413.         w = gtk.Window()
  1414.         w.connect('delete-event', gtk.main_quit)
  1415.         vb = gtk.VBox()
  1416.         hb = gtk.HBox()
  1417.         swallower = SwappableArea(hb)        
  1418.         tb = gtk.Toolbar()
  1419.         b = gtk.ToolButton(stock_id=gtk.STOCK_QUIT)
  1420.         b.connect('clicked',lambda x: w.hide() or gtk.main_quit())
  1421.         tb.add(b)
  1422.         def run_swallowed_dialog (*args):
  1423.             md = MessageDialog(title="Bar",label="Bar",sublabel="Baz "*12)
  1424.             swallower.run_dialog(md)
  1425.         b2 = gtk.ToolButton(label='Dialog')
  1426.         b2.connect('clicked',run_swallowed_dialog)
  1427.         tb.add(b2)
  1428.         vb.pack_start(tb,fill=False,expand=False)
  1429.         vb.pack_start(swallower,padding=12)
  1430.         w.add(vb)
  1431.         w.show_all()
  1432.         from gtk_goodies.dialog_extras import MessageDialog
  1433.         md = MessageDialog(title="Foo",label="Foo",sublabel="Bar "*12)
  1434.         swallower.run_dialog(md)
  1435.         hb.pack_start(sgd,padding=6)        
  1436.         sgd.change_grid(SudokuGrid(sample_open_sudoku),9)
  1437.         gtk.main()        
  1438.  
  1439.     def test_sudoku_game ():
  1440.         from sudoku import SudokuGrid, sample_open_sudoku
  1441.         sgd = SudokuGameDisplay(grid=SudokuGrid(sample_open_sudoku))
  1442.         sgd.set_bg_color('black')
  1443.         w = gtk.Window()
  1444.         w.connect('delete-event', gtk.main_quit)
  1445.         w.add(sgd)
  1446.         w.show_all()
  1447.         test_dance_grid(sgd)
  1448.         gtk.main()
  1449.         
  1450.     def test_number_selector ():
  1451.         w = gtk.Window()
  1452.         w.connect('delete-event',gtk.main_quit)
  1453.         ns = NumberSelector(default=3)
  1454.         def tell_me (b): print 'value->',b.get_value()
  1455.         ns.connect('changed',tell_me)
  1456.         w.add(ns)
  1457.         w.show_all()
  1458.         gtk.main()
  1459.  
  1460.     test_sudoku_game()
  1461.  
  1462.